/*
 * Copyright 2002-2010 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System.Data;
using Microsoft.Extensions.Logging;
using Spring.Messaging.Ems.Common;
using Spring.Messaging.Ems.Core;
using Spring.Objects.Factory;
using Spring.Transaction;
using Spring.Transaction.Support;

namespace Spring.Messaging.Ems.Connections;

/// <summary>
/// A <see cref="AbstractPlatformTransactionManager"/> implementation
/// for a single EMS <code>ConnectionFactory</code>. Binds a
/// Connection/Session pair from the specified ConnecctionFactory to the thread,
/// potentially allowing for one thread-bound Session per ConnectionFactory.
/// </summary>
/// <remarks>
/// <para>
/// Application code is required to retrieve the transactional Session via
/// <see cref="ConnectionFactoryUtils.GetTransactionalSession"/>.  Spring's
/// <see cref="EmsTemplate"/> will autodetect a thread-bound Session and
/// automatically participate in it.
/// </para>
/// <para>
/// Transaction synchronization is turned off by default, as this manager might be used
/// alongside an IDbProvider based Spring transaction manager such as the
/// AdoPlatformTransactionManager, which has stronger needs for synchronization.
/// </para>
/// </remarks>
/// <author>Juergen Hoeller</author>
/// <author>Mark Pollack (.NET)</author>
public class EmsTransactionManager : AbstractPlatformTransactionManager,
    IResourceTransactionManager, IInitializingObject
{
    private static readonly ILogger<EmsTransactionManager> LOG = LogManager.GetLogger<EmsTransactionManager>();

    private IConnectionFactory connectionFactory;

    /// <summary>
    /// Initializes a new instance of the <see cref="EmsTransactionManager"/> class.
    /// </summary>
    /// <remarks>
    /// The ConnectionFactory has to be set before using the instance.
    /// This constructor can be used to prepare a EmsTemplate via a ApplicationContext,
    /// typically setting the ConnectionFactory via ConnectionFactory property.
    /// <para>
    /// Turns off transaction synchronization by default, as this manager might
    /// be used alongside a dbprovider-based Spring transaction manager like
    /// AdoPlatformTransactionManager, which has stronger needs for synchronization.
    /// Only one manager is allowed to drive synchronization at any point of time.
    /// </para>
    /// </remarks>
    public EmsTransactionManager()
    {
        TransactionSynchronization = TransactionSynchronizationState.Never;
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="EmsTransactionManager"/> class
    /// given a ConnectionFactory.
    /// </summary>
    /// <param name="connectionFactory">The connection factory to obtain connections from.</param>
    public EmsTransactionManager(IConnectionFactory connectionFactory) : this()
    {
        ConnectionFactory = connectionFactory;
        AfterPropertiesSet();
    }

    /// <summary>
    /// Gets or sets the connection factory that this instance should manage transaction.
    /// for.
    /// </summary>
    /// <value>The connection factory.</value>
    public IConnectionFactory ConnectionFactory
    {
        get { return connectionFactory; }
        set
        {
            connectionFactory = value;
        }
    }

    /// <summary>
    /// Make sure the ConnectionFactory has been set.
    /// </summary>
    public void AfterPropertiesSet()
    {
        if (ConnectionFactory == null)
        {
            throw new ArgumentException("Property 'ConnectionFactory' is required.");
        }
    }

    /// <summary>
    /// Gets the resource factory that this transaction manager operates on,
    /// In tihs case the ConnectionFactory
    /// </summary>
    /// <value>The ConnectionFactory.</value>
    public object ResourceFactory
    {
        get { return ConnectionFactory; }
    }

    /// <summary>
    /// Get the EmsTransactionObject.
    /// </summary>
    /// <returns>he EmsTransactionObject.</returns>
    protected override object DoGetTransaction()
    {
        EmsTransactionObject txObject = new EmsTransactionObject();

        txObject.ResourceHolder =
            (EmsResourceHolder) TransactionSynchronizationManager.GetResource(ConnectionFactory);
        return txObject;
    }

    /// <summary>
    /// Begin a new transaction with the given transaction definition.
    /// </summary>
    /// <param name="transaction">Transaction object returned by
    /// <see cref="Spring.Transaction.Support.AbstractPlatformTransactionManager.DoGetTransaction"/>.</param>
    /// <param name="definition"><see cref="Spring.Transaction.ITransactionDefinition"/> instance, describing
    /// propagation behavior, isolation level, timeout etc.</param>
    /// <remarks>
    /// Does not have to care about applying the propagation behavior,
    /// as this has already been handled by this abstract manager.
    /// </remarks>
    /// <exception cref="Spring.Transaction.TransactionException">
    /// In the case of creation or system errors.
    /// </exception>
    protected override void DoBegin(object transaction, ITransactionDefinition definition)
    {
        //This is the default value defined in DefaultTransactionDefinition
        if (definition.TransactionIsolationLevel != IsolationLevel.ReadCommitted)
        {
            throw new InvalidIsolationLevelException("EMS does not support an isoliation level concept");
        }

        EmsTransactionObject txObject = (EmsTransactionObject) transaction;
        IConnection con = null;
        ISession session = null;
        try
        {
            con = CreateConnection();
            session = CreateSession(con);
            if (LOG.IsEnabled(LogLevel.Debug))
            {
                log.LogDebug("Created EMS transaction on Session [" + session + "] from Connection [" + con + "]");
            }

            txObject.ResourceHolder = new EmsResourceHolder(ConnectionFactory, con, session);
            txObject.ResourceHolder.SynchronizedWithTransaction = true;
            int timeout = DetermineTimeout(definition);
            if (timeout != DefaultTransactionDefinition.TIMEOUT_DEFAULT)
            {
                txObject.ResourceHolder.TimeoutInSeconds = timeout;
            }

            TransactionSynchronizationManager.BindResource(ConnectionFactory, txObject.ResourceHolder);
        }
        catch (EMSException ex)
        {
            if (session != null)
            {
                try
                {
                    session.Close();
                }
                catch (Exception)
                {
                }
            }

            if (con != null)
            {
                try
                {
                    con.Close();
                }
                catch (Exception) { }
            }

            throw new CannotCreateTransactionException("Could not create EMS Transaction", ex);
        }
    }

    /// <summary>
    /// Suspend the resources of the current transaction.
    /// </summary>
    /// <param name="transaction">Transaction object returned by
    /// <see cref="Spring.Transaction.Support.AbstractPlatformTransactionManager.DoGetTransaction"/>.</param>
    /// <returns>
    /// An object that holds suspended resources (will be kept unexamined for passing it into
    /// <see cref="Spring.Transaction.Support.AbstractPlatformTransactionManager.DoResume"/>.)
    /// </returns>
    /// <remarks>
    /// Transaction synchronization will already have been suspended.
    /// </remarks>
    /// <exception cref="Spring.Transaction.TransactionException">
    /// in case of system errors.
    /// </exception>
    protected override object DoSuspend(object transaction)
    {
        EmsTransactionObject txObject = (EmsTransactionObject) transaction;
        txObject.ResourceHolder = null;
        return TransactionSynchronizationManager.UnbindResource(ConnectionFactory);
    }

    /// <summary>
    /// Resume the resources of the current transaction.
    /// </summary>
    /// <param name="transaction">Transaction object returned by
    /// <see cref="Spring.Transaction.Support.AbstractPlatformTransactionManager.DoGetTransaction"/>.</param>
    /// <param name="suspendedResources">The object that holds suspended resources as returned by
    /// <see cref="Spring.Transaction.Support.AbstractPlatformTransactionManager.DoSuspend"/>.</param>
    /// <remarks>Transaction synchronization will be resumed afterwards.
    /// </remarks>
    /// <exception cref="Spring.Transaction.TransactionException">
    /// In the case of system errors.
    /// </exception>
    protected override void DoResume(object transaction, object suspendedResources)
    {
        EmsResourceHolder conHolder = (EmsResourceHolder) suspendedResources;
        TransactionSynchronizationManager.BindResource(ConnectionFactory, conHolder);
    }

    /// <summary>
    /// Perform an actual commit on the given transaction.
    /// </summary>
    /// <param name="status">The status representation of the transaction.</param>
    /// <exception cref="Spring.Transaction.TransactionException">
    /// In the case of system errors.
    /// </exception>
    protected override void DoCommit(DefaultTransactionStatus status)
    {
        EmsTransactionObject txObject = (EmsTransactionObject) status.Transaction;
        ISession session = txObject.ResourceHolder.GetSession();
        try
        {
            if (status.Debug)
            {
                LOG.LogDebug("Committing EMS transaction on Session [" + session + "]");
            }

            session.Commit();
        }
        catch (TransactionRolledBackException ex)
        {
            throw new UnexpectedRollbackException("EMS transaction rolled back", ex);
        }
        catch (EMSException ex)
        {
            throw new TransactionSystemException("Could not commit EMS transaction.", ex);
        }
    }

    /// <summary>
    /// Perform an actual rollback on the given transaction.
    /// </summary>
    /// <param name="status">The status representation of the transaction.</param>
    /// <exception cref="Spring.Transaction.TransactionException">
    /// In the case of system errors.
    /// </exception>
    protected override void DoRollback(DefaultTransactionStatus status)
    {
        EmsTransactionObject txObject = (EmsTransactionObject) status.Transaction;
        ISession session = txObject.ResourceHolder.GetSession();
        try
        {
            if (status.Debug)
            {
                LOG.LogDebug("Rolling back EMS transaction on Session [" + session + "]");
            }

            session.Rollback();
        }
        catch (EMSException ex)
        {
            throw new TransactionSystemException("Could not roll back EMS transaction.", ex);
        }
    }

    /// <summary>
    /// Set the given transaction rollback-only. Only called on rollback
    /// if the current transaction takes part in an existing one.
    /// </summary>
    /// <param name="status">The status representation of the transaction.</param>
    /// <exception cref="Spring.Transaction.TransactionException">
    /// In the case of system errors.
    /// </exception>
    protected override void DoSetRollbackOnly(DefaultTransactionStatus status)
    {
        EmsTransactionObject txObject = (EmsTransactionObject) status.Transaction;
        txObject.ResourceHolder.RollbackOnly = true;
    }

    /// <summary>
    /// Cleanup resources after transaction completion.
    /// </summary>
    /// <param name="transaction">Transaction object returned by
    /// <see cref="Spring.Transaction.Support.AbstractPlatformTransactionManager.DoGetTransaction"/>.</param>
    /// <remarks>
    /// <para>
    /// Called after <see cref="Spring.Transaction.Support.AbstractPlatformTransactionManager.DoCommit"/>
    /// and
    /// <see cref="Spring.Transaction.Support.AbstractPlatformTransactionManager.DoRollback"/>
    /// execution on any outcome.
    /// </para>
    /// </remarks>
    protected override void DoCleanupAfterCompletion(object transaction)
    {
        EmsTransactionObject txObject = (EmsTransactionObject) transaction;
        TransactionSynchronizationManager.UnbindResource(ConnectionFactory);
        txObject.ResourceHolder.CloseAll();
        txObject.ResourceHolder.Clear();
    }

    /// <summary>
    /// Check if the given transaction object indicates an existing transaction
    /// (that is, a transaction which has already started).
    /// </summary>
    /// <param name="transaction">Transaction object returned by
    /// <see cref="Spring.Transaction.Support.AbstractPlatformTransactionManager.DoGetTransaction"/>.</param>
    /// <returns>
    /// True if there is an existing transaction.
    /// </returns>
    /// <exception cref="Spring.Transaction.TransactionException">
    /// In the case of system errors.
    /// </exception>
    protected override bool IsExistingTransaction(object transaction)
    {
        EmsTransactionObject txObject = transaction as EmsTransactionObject;
        if (txObject != null)
        {
            return txObject.ResourceHolder != null;
        }

        return false;
    }

    /// <summary>
    /// Creates the connection via thie manager's ConnectionFactory.
    /// </summary>
    /// <returns>The new Connection</returns>
    /// <exception cref="EMSException">If thrown by underlying messaging APIs</exception>
    protected virtual IConnection CreateConnection()
    {
        return ConnectionFactory.CreateConnection();
    }

    /// <summary>
    /// Creates the session for the given Connection
    /// </summary>
    /// <param name="connection">The connection to create a Session for.</param>
    /// <returns>the new Session</returns>
    /// <exception cref="EMSException">If thrown by underlying messaging APIs</exception>
    protected virtual ISession CreateSession(IConnection connection)
    {
        return connection.CreateSession(true, Session.SESSION_TRANSACTED);
    }

    /// <summary>
    /// EMS Transaction object, representing a EmsResourceHolder.
    /// Used as transaction object by EMSTransactionManager
    /// </summary>
    internal class EmsTransactionObject : ISmartTransactionObject
    {
        private EmsResourceHolder resourceHolder;

        public EmsResourceHolder ResourceHolder
        {
            get { return resourceHolder; }
            set { resourceHolder = value; }
        }

        public bool RollbackOnly
        {
            get { return resourceHolder.RollbackOnly; }
        }
    }
}